﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;

namespace MVVMFramework.ViewModel
{
    public abstract class EditableObject<T> : NotifyPropertyChangedBase, IEditableObject where T : class
    {
        #region Fields

        
        private Dictionary<string, object> originalValues;

        #endregion

        protected EditableObject(T entity)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            Entity = entity;
        }

        public T Entity
        {
            get;
            private set;
        }

        private bool isInEditMode;
        public bool IsInEditMode
        {
            get
            {
                return isInEditMode;
            }
            private set
            {
                if (isInEditMode != value)
                {
                    isInEditMode = value;
                    OnPropertyChanged(() => IsInEditMode);
                }
            }
        }

        #region IEditableObject

        public void BeginEdit()
        {
            if (!IsInEditMode)
            {
                IsInEditMode = true;
                OnSaveOriginalValues();
                OnBeginEdit();
            }
        }

        public void CancelEdit()
        {
            if (IsInEditMode)
            {
                IsInEditMode = false;
                OnCancelEdit();

                RestoreOriginalValues();
                ClearOriginalValues();
            }
        }

        public void EndEdit()
        {
            if (IsInEditMode)
            {
                IsInEditMode = false;
                OnEndEdit();
                ClearOriginalValues();
            }
        }

        #endregion

        #region Protected Methods

        protected virtual void OnBeginEdit()
        {
        }

        protected virtual void OnCancelEdit()
        {
        }

        protected virtual void OnEndEdit()
        {

        }

        protected virtual void OnSaveOriginalValues()
        {

        }

        protected virtual void SaveOriginalValue(Expression<Func<object>> property)
        {
            string propertyName = GetPropertyPath(property);
            object value = getPropertyValue(propertyName);

            SaveOriginalValue(property, value);
        }

        protected virtual void SaveOriginalValue(Expression<Func<object>> property, object value)
        {
            if (originalValues == null)
                originalValues = new Dictionary<string, object>();

            string propertyPath = GetPropertyPath(property);
            originalValues.Add(propertyPath, value);
        }

        protected virtual void RestoreOriginalValues()
        {
            if (originalValues != null)
            {
                foreach (var originalValue in originalValues)
                {
                    string propertyName = originalValue.Key;
                    setPropertyValue(propertyName, originalValue.Value);
                }
            }
        }

        protected virtual void ClearOriginalValues()
        {
            if (originalValues != null)
                originalValues.Clear();
        }

        protected virtual bool? IsChanged(Expression<Func<object>> property)
        {
            if (originalValues == null)
                return null;

            string propertyPath = GetPropertyPath(property);

            object origValue = originalValues[propertyPath];
            object currentValue = getPropertyValue(propertyPath);

            return origValue != currentValue;
        }

        protected virtual bool? IsEntityChanged()
        {
            if (originalValues == null)
                return null;

            foreach (var originalValue in originalValues)
            {
                string propertyPath = originalValue.Key;

                object origValue = originalValues[propertyPath];
                object currentValue = getPropertyValue(propertyPath);

                if (object.Equals(origValue, currentValue) == false)
                    return true;
            }
            return false;
        }

        #endregion

        private void setPropertyValue(string property, object value)
        {
            Object obj = this;
            var path = property.Split('.');
            string currentPath = "";

            for (int n = 0; n < path.Length; n++)
            {
                string part = path[n];
                currentPath += ("." + part);

                bool isTargetObject = (n + 1 == path.Length);

                Type type = obj.GetType();
                PropertyInfo info = type.GetProperty(part);
                if (info == null)
                    throw new Exception(string.Format("Unable to get property {0} for path", currentPath));

                if (isTargetObject)
                {
                    info.SetValue(obj, Convert.ChangeType(value, info.PropertyType), null);
                }
                else
                {
                    obj = info.GetValue(obj, null);
                    if (obj == null)
                        throw new Exception(string.Format("Unable to get value {0} for path", currentPath));
                }
            }
        }

        private object getPropertyValue(string property)
        {
            Object obj = this;
            var path = property.Split('.');
            string currentPath = "";

            for (int n = 0; n < path.Length; n++)
            {
                string part = path[n];
                currentPath += ("." + part);

                bool isTargetObject = (n + 1 == path.Length);

                Type type = obj.GetType();
                PropertyInfo info = type.GetProperty(part);
                if (info == null)
                    throw new Exception(string.Format("Unable to get property {0} for path", currentPath));

                if (isTargetObject)
                {
                    return info.GetValue(obj, null);
                }

                obj = info.GetValue(obj, null);
                if (obj == null)
                    throw new Exception(string.Format("Unable to get value {0} for path", currentPath));
            }

            throw new Exception(string.Format("Unable to get value {0} for property", property));
        }
    }
}
